# encoding: utf-8
"""
@desc: 二分图推荐算法
@author: rannuo
@contact: rannuo1010@gmail.com
@time: 2019/4/23 20:57
@file: Prob.py
"""
import time
import math
import matplotlib.pyplot as plt

from alg import common


def gen_graph(train):
    """
    获取用户——物品二分图
    :param train:
    :return: 二分图字典:1.用户→商品→评分是否大于3 .2.商品→用户→评分是否大于3
    """
    G = dict()
    for u, items in train.items():
        for i in items:
            # 初始值
            value = 1
            if items[i] < 3.0:  # 评分小于3分置为0
                value = 0.5
            item_id = "item_" + str(i)
            if u not in G:
                G[u] = {}
            G[u][item_id] = value
            if item_id not in G:
                G[item_id] = {}
            G[item_id][u] = value
    return G


def recommend(G, user, N):
    """
    推荐
    :param G:
    :param user:
    :param N:
    :return:
    """
    '''Step1------物品拥有的资源值扩散到用户'''
    # 目标用户
    target = user
    user_res = {}
    for item in G[target]:  # 遍历目标用户选过的商品集合
        k_item = len(G[item])  # 物品的度
        for user in G[item]:  # 遍历选择过该物品的用户
            pre_value = user_res.setdefault(user, 0)
            user_res[user] = G[item][user] / float(k_item) + pre_value

    '''Step2 - 用户得到的资源值扩散到物品'''
    item_res = {}
    for user in user_res.keys():
        energy = user_res[user]
        k_user = len(G[user])  # 用户的度
        for item in G[user]:
            pre_value = item_res.setdefault(item, 0)
            item_res[item] = (energy / float(k_user) + pre_value)
    # 过滤掉目标用户选择过的物品
    for item in G[target]:
        item_res[item] = 0.0

    # 返回能量最多的前n个物品推荐给目标用户
    return sorted(item_res.items(), key=lambda x: x[1], reverse=True)[0:N]


def calc_recall(train, test, N, G):
    """
    计算召回率
    召回率描述有多少比例的用户—物品评分记录包含在最终的推荐列表中
    通过【准确率/召回率】评测推荐算法的精度
    :param train:训练集
    :param test:测试集
    :param N:TopN推荐中N数目
    :return:召回率
    """
    hit = 0  # 预测准确的数目
    total = 0  # 所有行为总数
    count = 1
    for user in train.keys():
        print('\r用户{}'.format(count), end='')
        count += 1
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        rank = recommend(G, user, N)
        for item, _ in rank:
            item_id = int(item.split('_')[1])
            if item_id in tu:
                hit += 1
        total += len(tu)
    return hit / (total * 1.0)


def calc_precision(train, test, N, G):
    """
    计算准确率
    准确率描述最终的推荐列表中有多少比例是发生过的用户—物品评分记录
    :param train:训练集
    :param test:测试集
    :param N:TopN推荐中N数目
    :return:准确率
    """
    hit = 0  # 预测准确的数目
    total = 0  # 所有行为总数
    for user in train.keys():
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        rank = recommend(G, user, N)
        for item, _ in rank:
            item_id = int(item.split('_')[1])
            if item_id in tu:
                hit += 1
        total += N
    return hit / (total * 1.0)


def calc_coverage(train, N, G):
    """
    计算覆盖率
    覆盖率反映了推荐算法发掘长尾的能力，覆盖率越高，说明推荐算法越能够将长尾中的物品推荐给用户
    :param train:训练集 字典user->items
    :param N: topN推荐中N
    """
    recommend_items = set()
    all_items = set()
    for user in train.keys():
        for item in train[user].keys():
            all_items.add(item)
        rank = recommend(G, user, N)
        for item, _ in rank:
            item_id = int(item.split('_')[1])
            recommend_items.add(item_id)
    return len(recommend_items) / (len(all_items) * 1.0)


def calc_popularity(train, N, G):
    """
    计算平均流行度 信息的熵
    如果推荐出的物品都很热门，说明推荐的新颖度较低，否则说明推荐结果比较新颖
    :param train:训练集 字典user->items
    :param N:topN推荐中N
    :return:平均流行度
    """
    item_popularity = dict()
    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity.setdefault(item, 0)
            item_popularity[item] += 1
    ret = 0
    n = 0
    for user in train.keys():
        rank = recommend(G, user, N)
        for item, _ in rank:
            item_id = int(item.split('_')[1])
            # 流行度分布满足长尾分布，在取对数后，流行度的平均值更加稳定
            ret += math.log(1 + item_popularity[item_id])
            n += 1
    return ret / (n * 1.0)


def calc_accuracy(train, test, N, G):
    hit = 0
    recall_total = 0
    precision_total = 0
    for user in train.keys():
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        rank = recommend(G, user, N)
        for item, _ in rank:
            item_id = int(item.split('_')[1])
            if item_id in tu:
                hit += 1
        recall_total += len(tu)
        precision_total += N
    recall = hit / recall_total * 1.0
    precision = hit / precision_total * 1.0
    # return 2 * recall * precision / (recall + precision)
    return precision / recall


def calc_evaluation_index(train, test, G, N):
    """
    计算评测指标
    :param train: 训练集
    :param test: 测试集
    :param G: 二分图
    :param N: 推荐列表长度(Top-N)
    :return: 召回率、准确率、覆盖率、平均流行度、算法精度
    """
    hit = 0
    tu_total = 0
    ru_total = 0
    popularity_ret = 0
    recommend_items = set()
    all_items = set()
    item_popularity = dict()

    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity.setdefault(item, 0)
            item_popularity[item] += 1

    for user in train.keys():
        for item in train[user].keys():
            all_items.add(item)
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        ru = recommend(G, user, N)
        for item, _ in ru:
            item_id = int(item.split('_')[1])
            if item_id in tu:
                hit += 1
            popularity_ret += math.log(1 + item_popularity[item_id])  # 流行度分布满足长尾分布，在取对数后，流行度的平均值更加稳定
            recommend_items.add(item_id)
        tu_total += len(tu)
        ru_total += N

    recall = hit / tu_total * 1.0  # 计算召回率
    precision = hit / ru_total * 1.0  # 计算准确率
    coverage = len(recommend_items) / len(all_items) * 1.0  # 计算覆盖率
    popularity = popularity_ret / ru_total  # 计算流行度
    accuracy = precision / recall  # 计算精确度
    return recall, precision, coverage, popularity, accuracy


if __name__ == "__main__":
    time_1 = time.time()
    M = 5
    k = 1
    seed = 1
    n_list = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]  # 训练参数，参数的值

    print('读数据→', end='')
    data = common.get_data("../data/ratings.txt")
    train, test, _, _ = common.split_data(data, M, k, seed)

    print('生成字典→', end='')
    G = gen_graph(train)

    time_2 = time.time()

    print('一次推荐→', end='')
    prob_recall = []  # prob召回率
    for N in n_list:
        print('计算召回率→')
        recall = calc_recall(train, test, N, G)
        print('\n', N, ':', recall)
        prob_recall.append(recall)

    # 绘制图表
    x = n_list
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.plot(x, prob_recall, label='Prob', linewidth=1, color='r', marker='^', markerfacecolor='m', markersize=7)
    plt.xlabel('L')
    plt.ylabel('Recall Rate')
    plt.title('召回率折线图')
    plt.legend()
    plt.show()
